IT邦第二篇 就獻給委派了
記得當年第一次看到 += 這東西的時候
問問前輩這是什麼
前輩只有跟我說 : 委派 很可怕 不要用~~~
真正深入了解之後才覺得相見恨晚啊!
先從委派最正規的方式寫起(但我幾乎不用這種方式...)
public delegate void DoSomething(int number);
先宣告一個委派的簽章 這個簽章代表晚點要賦予它的方法
必須符合 無回傳值(void) 且 具有一個參數 int
換句話說 如果宣告成如下
public delegate int ParseSomething(string str);
就代表 該方法必須回傳int 且 具有一個參數 string
public static void PrintNumber(int n)
{
Console.WriteLine(n);
}
public static void SquareAndPrintNumber(int n)
{
n *= n;
Console.WriteLine(n);
}
static void Main(string[] args)
{
var something = new DoSomething(PrintNumber);
something.Invoke(5);
something(6);
}
我宣告了兩個方法 PrintNumber 跟 SquareAndPrintNumber
都符合DoSomething的簽章 (無回傳 且具有一個參數int)
在這個範例中 先使用PrintNumber
var something = new DoSomething(PrintNumber);就是建立一個委派 並把PrintNumber傳入
something.Invoke(5);就是真正去執行這個委派 PrintNumber 所以最後會印出 5
something(6); 是執行委派的另外一個方式 所以會印出6
我們修改一下程式碼 將 PrintNumber更換成 SquareAndPrintNumber
static void Main(string[] args)
{
var something = new DoSomething(SquareAndPrintNumber);
something.Invoke(5);
something(6);
}
這時候要執行的方法就會變成SquareAndPrintNumber
所以印出來會是 25 36
我們再做一個實驗 如果有兩個委派 具有相同的簽章 他們是否可以互通呢?
我們修改一下程式碼 新增一個DoSomething1 跟 DoSomething一模一樣 只是名稱不同
再修改一下呼叫委派的方式
public delegate void DoSomething1(int number);
public static void ExeDoSomething(DoSomething something)//呼叫委派的方式
{
something.Invoke(5);
something(6);
}
static void Main(string[] args)
{
{
var something = new DoSomething(PrintNumber);
ExeDoSomething(something);
}
{
var something = new DoSomething1(PrintNumber);
ExeDoSomething(something);//error
}
}
會發現 就算簽章一樣 只要宣告的委派不同 他們之間是不可以互相轉換的~相當的嚴謹
照上面正規委派寫法 我可能一個方法就必須要建立一個public delegate void DoSomething1(int number);這種委派簽章
又臭又長 使用上又不方便
接下來 就是巨硬的德政了!Action/Func
Action/Func 是泛型的委派(泛型之後會再開一篇來講 不要在這邊亂開副本!)
我如果需要一個無回傳值且具有一個int參數的方法 我不需要從頭宣告一個
public delegate void DoSomething1(int number);來用
我只要寫Action 就可以了
Action action1;//無回傳值無參數
Action<int> action2;//無回傳值具有一個int參數
Action<int, string> action3;//無回傳值具有int string參數
Func<int> func1;//回傳int 無參數
Func<string,int> func2;//回傳int 具有一個string參數
Func<int,string,DateTime> func3;//回傳DateTime具有int string參數
上面這些宣告可以自行體會一下
我們可以很容易地知道 Action就是無回傳值的委派 而Func就是有回傳值的
其餘用法幾乎沒有差異
附帶一提 以前最多支援到8個型別參數 也就是說我最多可以寫
Action<int,string,double,float,Datetime,long,List,bool> //應該沒人會這樣寫..吧?
但剛剛看了一下程式碼.Net6 已經可以支援到16個參數型別了呢!(灑花?
回歸正題
修改一下程式碼 將 ExeDoSomething的參數從 DoSomething 更換成 Action
public static void ExeDoSomething(Action<int> something)
{
something.Invoke(5);
something(6);
}
static void Main(string[] args)
{
ExeDoSomething(PrintNumber);
ExeDoSomething(SquareAndPrintNumber);
}
因為 PrintNumber 跟 SquareAndPrintNumber 都是符合 無回傳值 有一個int參數的方法簽章
所以他們都可以當作參數傳入ExeDoSomething
執行結果會是 5, 6, 25, 36
喔~ 可是還是好煩阿 要把方法當參數傳入 我必須要建立一個方法才能這樣做
想名字應該是程序猿永遠的痛吧..
還好還好 巨硬的德政 匿名委派 我們可以這樣寫
上面那兩個方法可以砍掉了(PrintNumber/SquareAndPrintNumber)
static void Main(string[] args)
{
ExeDoSomething((int n) =>
{
Console.WriteLine(n);
});
ExeDoSomething((int n) =>
{
n *= n;
Console.WriteLine(n);
});
}
(int n) 這邊可以想成就是方法後面的參數簽章 前面沒有名字 => 後面是方法主體
這種lambda寫法在巨硬很常使用,建議要習慣一下!
喔喔喔喔!! 不用想名字了真好~ 剛剛光想PrintNumber/SquareAndPrintNumber 就花了我兩小時呢!
修但幾累!
巨硬表示 我覺得這樣看起來還是有點蠢
因為ExeDoSomething 裡面的參數就已經知道 Action的參數是int 為什麼你外面還要寫一次?
ExeDoSomething((n) =>
{
Console.WriteLine(n);
});
巨硬表示 : 我覺得只有一個參數n還要加括號有點蠢
ExeDoSomething(n =>
{
Console.WriteLine(n);
});
巨硬表示 : 程式本體只有一行 應該可以再精簡吧?
ExeDoSomething(n => Console.WriteLine(n));
流浪漢表示 : (n)後面沒加分號 (這裡不用加啊!!!!!
這種寫法在巨硬稱做lambda表達式
上面這種寫法 有幾個限制
第一 參數要一個才能省略參數的括號
第二 如果沒參數的畫 一定要有括號
第三 程式碼如果只有一行 可以省略該行的分號以及程式本體的大括號({})(分號 大括號必須同時存在/省略)
參數型別都可以省略
Func<int> func = () => 1;//因為無參數 所以都是回傳1;
Func<int,int> func2 = x => x+1;//具有參數 回傳 x+1
委派基礎就到這邊!
下一篇預計會開委派的實作
因為知道委派但是不知道怎麼去活用它還是沒用阿!!!
------3/8號補充-----
本來一直記得要講這個重要的東西
但居然忘記了...Orz(這年代還有人用這個嗎?
委派的 閉包問題
先上Code
static void Main(string[] args)
{
Action action = null;
for(int i = 0; i < 10; i++)
{
action += () => Console.WriteLine(i);
}
action();
}
會印出什麼?
0 1 2 3 4 5 6 7 8 9 ??
不對
會是
10 10 10 10 10 10 10 10 10 10
Why?
因為委派的方法是 印出i
但我只是去設定委派內容
真正在執行的是 action(); 這一行
而i跑完迴圈 for(int i = 0; i < 10; i++) 跳離迴圈時會是10
所以真正執行的時候 是印出 10
那我真正要印出0~9怎麼辦?
給他一個暫存變數即可
static void Main(string[] args)
{
Action action = null;
for(int i = 0; i <10;i++)
{
var temp = i;
action += () => Console.WriteLine(temp);
}
action();
}
這樣 其實記憶體會產生10個temp 而每個temp分別就是 0-9
最後執行的時候就是執行印出各自的temp
使用委派要特別注意執行的時機跟變數的值喔!
學過 JS GO 或任何一級函式語言的開發者應該可以輕鬆理解
C# 的委派換句話說就是想要把函式當作變數傳遞